<!DOCTYPE html>
<html>

<head>
  <title>canvas test-28 - 绘制圆角矩形/虚线/箭头</title>
  <style type="text/css">
  * {
    margin: 0;
    padding: 0
  }
  
  pre {
    padding: 10px 0
  }
  </style>
</head>

<body>
  <pre>
    绘制圆角矩形/虚线/箭头
  </pre>
  <p></p>
  <canvas id="stage" width="640" height="480" style="border: 1px solid red; margin: 20px"></canvas>
  <script src="raf.js"></script>
  <script src="vector2.js"></script>
  <script src="resource.js"></script>
  <script type="text/javascript">
  var canvas = document.getElementById('stage');
  var ctx = canvas.getContext('2d');

  var canvasWidth = canvas.width;
  var canvasHeight = canvas.height;
  var bbox = canvas.getBoundingClientRect();
  var PI2 = Math.PI * 2;
  var ARC = Math.PI / 180;
  var DEG = 180 / Math.PI;
  var centerX = canvasWidth / 2;
  var centerY = canvasHeight / 2;

  window.onload = rendering;

  function rendering(disableLight) {
    ctx.clearRect(0, 0, canvasWidth + 1, canvasHeight + 1);

    // draw rounded rect 5px arc
    drawRoundRect(20, 20, 80, 60, 5);
    ctx.stroke();

    // draw rounded rect 10px arc
    ctx.save();
    drawRoundRect(120, 20, 80, 60, 10);
    ctx.fillStyle = 'rgba(255,0,0,.5)';
    ctx.fill();
    ctx.restore();

    // draw dash line
    ctx.save();
    ctx.strokeStyle = 'red';
    drawDashLine(220, 20, 600, 20, 380); // #0
    drawDashLine(220, 40, 600, 40, 10, 3); // #1
    drawDashLine(220, 50, 600, 50, 10, 4); // # 2
    drawDashLine(220, 60, 600, 60, 10, 5); // #3
    drawDashLine(220, 70, 600, 70, 10, 6); // #4
    drawDashLine(220, 80, 600, 80, 10, 7); // #5
    drawDashLine(220, 90, 600, 90, 10, 8); // #6
    drawDashLine(220, 100, 600, 100, 10, 9); // #7
    drawDashLine(220, 110, 600, 110, 20, 10); // #8
    drawDashLine(20, 130, 400, 230, 20, 10); // #9
    drawDashLine(20, 230, 200, 130); // #10
    drawDashLine(50, 240, 210, 140, 10, 5); // #11
    drawDashLine(300, 130, 300, 230); // #12
    drawDashLine(220, 30, 600, 30, 10, 3); // #13
    ctx.restore();

    // draw dash rect
    drawDashRect(400, 140, 100, 60);
    drawDashRect(520, 140, 90, 120);

    // // draw line with arrow
    drawArrowLine(50, 260, 120, 360, 10, 36);
    drawArrowLine(50, 360, 120, 260, 10, 36);
    drawArrowLine(170, 250, 175, 360, 10);
    drawArrowLine(200, 360, 200, 250, 10);
    drawArrowLine(350, 250, 250, 360, 15, 70);
    drawArrowLine(350, 360, 250, 250, 15, 100);
    drawArrowLine(400, 360, 600, 300, 30, 50);
  }

  function drawRoundRect(x, y, width, height, radius) {
    ctx.beginPath();
    ctx.moveTo(x, y + radius);
    ctx.quadraticCurveTo(x, y, x + radius, y);
    ctx.lineTo(x + width - radius, y);
    ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
    ctx.lineTo(x + width, y + height - radius);
    ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
    ctx.lineTo(x + radius, y + height);
    ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
    ctx.lineTo(x, y + radius);
    ctx.closePath();
  }

  function drawDashLine(fromX, fromY, toX, toY, dashSize, dashGap) {
    // normalize arguments
    dashSize = dashSize || 5;
    dashGap = dashGap || 3;

    var deltaX = toX - fromX;
    var deltaY = toY - fromY;
    var distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
    var cosa = deltaX / distance;
    var sina = deltaY / distance;
    var dashStep = dashSize + dashGap;
    var len = (distance / dashStep) << 0;
    var rest = distance % dashStep;
    var curX = fromX;
    var curY = fromY;
    var fixup = 0;
    var fixstart = true;
    var fixend = true;

    // ensure the line start and end with line instead of gap
    if (rest === 0) {
      fixup = dashSize / 2;
      len -= 1;
    } else if (rest > dashSize) {
      fixup = (rest - dashGap) / 2;
    } else {
      fixup = rest;
      fixstart = false;
    }

    ctx.beginPath();

    if (fixstart) {
      ctx.moveTo(curX, curY);
      curX += fixup * cosa;
      curY += fixup * sina;
      ctx.lineTo(curX, curY);

      curX += dashGap * cosa;
      curY += dashGap * sina;
    }

    for (var i = 0; i < len; ++i) {
      ctx.moveTo(curX, curY);
      curX += dashSize * cosa;
      curY += dashSize * sina;
      ctx.lineTo(curX, curY);

      curX += dashGap * cosa;
      curY += dashGap * sina;
    }

    if (fixend) {
      ctx.moveTo(curX, curY);
      curX += fixup * cosa;
      curY += fixup * sina;
      ctx.lineTo(curX, curY);
    }

    ctx.stroke();
  }

  function drawDashRect(x, y, width, height) {
    ctx.save();
    ctx.strokeStyle = 'blue';
    drawDashLine(x, y, x + width, y);
    drawDashLine(x + width, y, x + width, y + height);
    drawDashLine(x + width, y + height, x, y + height);
    drawDashLine(x, y + height, x, y);
    ctx.restore();
  }

  // 方法1: 如果以直线作为一个坐标系的x轴（可理解为将直线旋转为canvas的原始坐标系） 则箭头的两侧相当于绕x轴旋转相应的角度
  // 因此分别减去箭头两侧的向量投影到对应坐标轴上的分量即为对应箭头终点的坐标
  // 该方法调用ctx api较少，没有开根、大量除法计算
  function drawArrowLine(fromX, fromY, toX, toY, arrowSide, arrowDeg) {
    // apply default config
    arrowSide = arrowSide || 10;
    arrowDeg = arrowDeg || 50;

    var deltaX = toX - fromX;
    var deltaY = toY - fromY;
    var arc = Math.atan2(deltaY, deltaX);
    var thed = arrowDeg / 2 * ARC;
    var arc1 = arc - thed;
    var arc2 = arc + thed;
    var p1x = toX - arrowSide * Math.cos(arc1);
    var p1y = toY - arrowSide * Math.sin(arc1);
    var p2x = toX - arrowSide * Math.cos(arc2);
    var p2y = toY - arrowSide * Math.sin(arc2);

    ctx.beginPath();
    ctx.moveTo(fromX, fromY);
    ctx.lineTo(toX, toY);
    ctx.lineTo(p1x, p1y);
    ctx.moveTo(toX, toY);
    ctx.lineTo(p2x, p2y);
    ctx.stroke();
  }

  // 方法2: 如果以直线作为坐标轴，那么平移旋转坐标轴后，箭头的两侧相当于顺时针、逆时针旋转相应的角度
  // 该方法简单容易理解，没有复杂的计算， 缺点是调用较多的ctx api
  function drawArrowLine2(fromX, fromY, toX, toY, arrowSide, arrowDeg) {
    // apply default config
    arrowSide = arrowSide || 10;
    arrowDeg = arrowDeg || 40;

    var deltaX = toX - fromX;
    var deltaY = toY - fromY;
    var arc = Math.atan2(deltaY, deltaX);
    var sina = Math.sin(arrowDeg * ARC / 2);
    var cosa = Math.cos(arrowDeg * ARC / 2);

    ctx.beginPath();
    ctx.moveTo(fromX, fromY);
    ctx.lineTo(toX, toY);
    ctx.stroke();
    ctx.save();
    ctx.translate(toX, toY);
    ctx.rotate(arc);
    ctx.beginPath();
    ctx.moveTo(0, 0);
    ctx.lineTo(-arrowSide * cosa, -arrowSide * sina);
    ctx.moveTo(0, 0);
    ctx.lineTo(-arrowSide * cosa, arrowSide * sina);
    ctx.restore();
    ctx.stroke();
  }

  // 方法3: 理由已知直线的角度，分别顺、逆时针旋转对应的角度，可取得箭头两侧对应直线的斜率
  // 已知直线斜率，直线上一点坐标p(x,y)以及点p'距点p的距离，求点p'的坐标就很简单了
  // 该方法调用ctx api 较少，但涉及到两侧开根计算且存在平行于y轴的情况，需要额外处理，不推荐该方法
  function drawArrowLine3(fromX, fromY, toX, toY, arrowSide, arrowDeg) {
    // apply default config
    arrowSide = arrowSide || 10;
    arrowDeg = arrowDeg || 40;

    var deltaX = toX - fromX;
    var deltaY = toY - fromY;
    var arc = Math.atan2(deltaY, deltaX);
    var arcv = Math.PI / 2;
    var arc1 = arc - arrowDeg * ARC / 2;
    var arc2 = arc + arrowDeg * ARC / 2;
    var r1 = Math.abs(arc1) > arcv ? -1 : 1; // 如果角度不在－90 ~ 90间，需要调整方向
    var r2 = Math.abs(arc2) > arcv ? -1 : 1;
    var k1 = Math.tan(arc1);
    var k2 = Math.tan(arc2);
    var p1x = toX - arrowSide / Math.sqrt(1 + k1 * k1) * r1;
    var p1y = toY - arrowSide * k1 / Math.sqrt(1 + k1 * k1) * r1;
    var p2x = toX - arrowSide / Math.sqrt(1 + k2 * k2) * r2;
    var p2y = toY - arrowSide * k2 / Math.sqrt(1 + k2 * k2) * r2;

    ctx.beginPath();
    ctx.moveTo(fromX, fromY);
    ctx.lineTo(toX, toY);
    ctx.lineTo(p1x, p1y);
    ctx.moveTo(toX, toY);
    ctx.lineTo(p2x, p2y);
    ctx.stroke();
  }
  </script>
</body>

</html>
